package com.acgist.snail.net.torrent.tracker;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import com.acgist.snail.config.PeerConfig;
import com.acgist.snail.config.SystemConfig;
import com.acgist.snail.config.TrackerConfig;
import com.acgist.snail.format.BEncodeDecoder;
import com.acgist.snail.logger.Logger;
import com.acgist.snail.logger.LoggerFactory;
import com.acgist.snail.net.NetException;
import com.acgist.snail.net.http.HttpClient;
import com.acgist.snail.net.torrent.TorrentSession;
import com.acgist.snail.protocol.Protocol;
import com.acgist.snail.utils.MapUtils;
import com.acgist.snail.utils.PeerUtils;
import com.acgist.snail.utils.StringUtils;

/**
 * HTTP Tracker信息
 * 协议链接：https://wiki.theory.org/index.php/BitTorrentSpecification
 * The BitTorrent Protocol Specification
 * 协议链接：http://www.bittorrent.org/beps/bep_0003.html
 * Tracker Returns Compact Peer Lists
 * 协议链接：http://www.bittorrent.org/beps/bep_0023.html
 * Tracker Protocol Extension: Scrape
 * 协议链接：http://www.bittorrent.org/beps/bep_0048.html
 * 
 * @author acgist
 */
public final class HttpTrackerSession extends TrackerSession {

    private static final Logger LOGGER = LoggerFactory.getLogger(HttpTrackerSession.class);
    
    /**
     * 刮擦地址：{@value}
     */
    private static final String SCRAPE_URL_SUFFIX = "/scrape";
    /**
     * 声明地址：{@value}
     */
    private static final String ANNOUNCE_URL_SUFFIX = "/announce";
    
    /**
     * 明文IP地址：{@value}
     * 例子：192.168.1.100
     */
    public static final String IP_ADDRESS = "0";
    /**
     * 紧凑IP地址：{@value}
     * 例子：192.168.1.100 -> 3232235876
     */
    public static final String IP_COMPACT = "1";
    
    /**
     * 跟踪器ID
     */
    private String trackerId;
    
    /**
     * @param scrapeUrl   刮擦地址
     * @param announceUrl 声明地址
     * 
     * @throws NetException 网络异常
     */
    private HttpTrackerSession(String scrapeUrl, String announceUrl) throws NetException {
        super(scrapeUrl, announceUrl, Protocol.Type.HTTP);
    }

    /**
     * 新建HTTP Tracker信息
     * 
     * @param announceUrl 声明地址
     * 
     * @return {@link HttpTrackerSession}
     * 
     * @throws NetException 网络异常
     */
    public static final HttpTrackerSession newInstance(String announceUrl) throws NetException {
        final String scrapeUrl = buildScrapeUrl(announceUrl);
        return new HttpTrackerSession(scrapeUrl, announceUrl);
    }

    @Override
    public void started(Integer sid, TorrentSession torrentSession) throws NetException {
        final String announceMessage = (String) this.buildAnnounceMessage(sid, torrentSession, TrackerConfig.Event.STARTED);
        final var client = HttpClient
            .newInstance(announceMessage)
            .get();
        if(!client.ok()) {
            throw new NetException("HTTP Tracker声明失败");
        }
        final var body = client.responseToBytes();
        final var decoder = BEncodeDecoder.newInstance(body).next();
        if(decoder.isEmpty()) {
            throw new NetException("HTTP Tracker声明消息错误（格式）：" + new String(body));
        }
        final var message = convertAnnounceMessage(sid, decoder);
        // 初始化跟踪器ID
        this.trackerId = message.trackerId();
        TrackerContext.getInstance().announce(message);
    }
    
    @Override
    public void completed(Integer sid, TorrentSession torrentSession) throws NetException {
        HttpClient.newInstance((String) this.buildAnnounceMessage(sid, torrentSession, TrackerConfig.Event.COMPLETED)).get();
    }
    
    @Override
    public void stopped(Integer sid, TorrentSession torrentSession) throws NetException {
        HttpClient.newInstance((String) this.buildAnnounceMessage(sid, torrentSession, TrackerConfig.Event.STOPPED)).get();
    }
    
    @Override
    public void scrape(Integer sid, TorrentSession torrentSession) throws NetException {
        final String scrapeMessage = this.buildScrapeMessage(sid, torrentSession);
        if(scrapeMessage == null) {
            throw new NetException("HTTP Tracker刮擦失败：" + this.scrapeUrl);
        }
        final var client = HttpClient
            .newInstance(scrapeMessage)
            .get();
        if(!client.ok()) {
            throw new NetException("HTTP Tracker刮擦失败");
        }
        final var body = client.responseToBytes();
        final var decoder = BEncodeDecoder.newInstance(body).next();
        if(decoder.isEmpty()) {
            throw new NetException("HTTP Tracker刮擦消息错误（格式）：" + new String(body));
        }
        final var messages = convertScrapeMessage(sid, decoder);
        messages.forEach(TrackerContext.getInstance()::scrape);
    }
    
    @Override
    protected String buildAnnounceMessageEx(Integer sid, TorrentSession torrentSession, TrackerConfig.Event event, long upload, long download, long left) {
        final StringBuilder builder = new StringBuilder(this.announceUrl);
        builder.append("?")
            .append("info_hash").append("=").append(torrentSession.infoHash().getInfoHashUrl()).append("&")
            .append("peer_id").append("=").append(PeerConfig.getInstance().getPeerIdUrl()).append("&")
            .append("port").append("=").append(SystemConfig.getTorrentPortExtShort()).append("&")
            .append("uploaded").append("=").append(upload).append("&")
            .append("downloaded").append("=").append(download).append("&")
            .append("left").append("=").append(left).append("&")
            .append("compact").append("=").append(IP_COMPACT).append("&")
            .append("event").append("=").append(event.getValue()).append("&")
            .append("numwant").append("=").append(WANT_PEER_SIZE);
        if(StringUtils.isNotEmpty(this.trackerId)) {
            // 跟踪器ID
            builder.append("&").append("trackerid").append("=").append(this.trackerId);
        }
        return builder.toString();
    }
    
    /**
     * 新建刮擦消息
     * 
     * @param sid            {@link TrackerLauncher#id()}
     * @param torrentSession BT任务信息
     * 
     * @return 刮擦消息
     */
    private String buildScrapeMessage(Integer sid, TorrentSession torrentSession) {
        if(StringUtils.isEmpty(this.scrapeUrl)) {
            return null;
        }
        final StringBuilder builder = new StringBuilder(this.scrapeUrl);
        builder.append("?")
            .append("info_hash").append("=").append(torrentSession.infoHash().getInfoHashUrl());
        return builder.toString();
    }

    /**
     * 解析声明消息
     * 
     * @param sid     {@link TrackerLauncher#id()}
     * @param decoder B编码解码器
     * 
     * @return 声明消息
     */
    private static final AnnounceMessage convertAnnounceMessage(Integer sid, BEncodeDecoder decoder) {
        final String failureReason = decoder.getString("failure reason");
        if(StringUtils.isNotEmpty(failureReason)) {
            LOGGER.warn("HTTP Tracker声明失败：{}", failureReason);
        }
        final String warngingMessage = decoder.getString("warnging message");
        if(StringUtils.isNotEmpty(warngingMessage)) {
            LOGGER.warn("HTTP Tracker声明警告：{}", warngingMessage);
        }
        final Map<String, Integer> peers = new HashMap<>();
        final var peersIPv4 = PeerUtils.readIPv4(decoder.get("peers"));
        final var peersIPv6 = PeerUtils.readIPv6(decoder.get("peers6"));
        peers.putAll(peersIPv4);
        peers.putAll(peersIPv6);
        return AnnounceMessage.newHttp(
            sid,
            decoder.getString("tracker id"),
            decoder.getInteger("interval"),
            decoder.getInteger("min interval"),
            decoder.getInteger("incomplete"),
            decoder.getInteger("complete"),
            peers
        );
    }
    
    /**
     * 解析刮擦消息
     * 
     * @param sid     {@link TrackerLauncher#id()}
     * @param decoder B编码解码器
     * 
     * @return 刮擦消息
     */
    private static final List<ScrapeMessage> convertScrapeMessage(Integer sid, BEncodeDecoder decoder) {
        final var files = decoder.getMap("files");
        if(files == null) {
            return List.of();
        }
        return files.entrySet().stream()
            .map(entry -> {
                // key：InfoHash
                final Map<?, ?> map = (Map<?, ?>) entry.getValue();
                return ScrapeMessage.newInstance(
                    sid,
                    MapUtils.getInteger(map, "complete"),
                    MapUtils.getInteger(map, "downloaded"),
                    MapUtils.getInteger(map, "incomplete")
                );
            })
            .collect(Collectors.toList());
    }
    
    /**
     * 声明地址转为刮擦地址
     * 
     * ~http://example.com/announce            ~http://example.com/scrape
     * ~http://example.com/x/announce          ~http://example.com/x/scrape
     * ~http://example.com/announce.php        ~http://example.com/scrape.php
     * ~http://example.com/announce?x2%0644    ~http://example.com/scrape?x2%0644
     * ~http://example.com/a                   (scrape not supported)
     * ~http://example.com/x%064announce       (scrape not supported)
     * ~http://example.com/announce?x=2/4      (scrape not supported)
     * 
     * @param url 声明地址
     * 
     * @return 刮擦地址
     */
    private static final String buildScrapeUrl(String url) {
        if(url != null && url.contains(ANNOUNCE_URL_SUFFIX)) {
            return url.replace(ANNOUNCE_URL_SUFFIX, SCRAPE_URL_SUFFIX);
        }
        return null;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(this.announceUrl);
    }
    
    @Override
    public boolean equals(Object object) {
        if(this == object) {
            return true;
        }
        if(object instanceof HttpTrackerSession session) {
            return StringUtils.equals(this.announceUrl, session.announceUrl);
        }
        return false;
    }
    
}
